Les listes chainées se libèrent de la contrainte de stocker les éléments dans des emplacements contigus en mémoire.
Bénéfice: Aucun déplacement nécessaire pour insérer ou supprimer
Coûts:
Les éléments de la liste sont stockés indivuellement dans les maillons de la chaine. Chaque maillon stocke
class Maillon:
def __init__(self,valeur):
self.donnee = valeur
self.suivant = None
Cette structure suffit à constuire une liste manuellement. Construisons
tete = Maillon(12)
tete.suivant = Maillon(99)
tete.suivant.suivant = Maillon(37)
Pour traverser tous les éléments d'une liste, il faut
On peut l'écrire récursivement
def afficher(M):
if M != None:
print(M, end = " → ")
afficher(M.suivant)
else:
print("⌀")
afficher(tete)
12 → 99 → 37 → ⌀
Mieux, on peut l'écrire itérativement
def afficher(M):
while M != None:
print(M, end = " → ")
M = M.suivant
print("⌀")
afficher(tete)
12 → 99 → 37 → ⌀
Insérer et supprimer en tête sont des opérations simples.
Pour insérer, on doit
def inserer_en_tete(ancienne_tete, valeur):
nouvelle_tete = Maillon(valeur)
nouvelle_tete.suivant = ancienne_tete
return nouvelle_tete
afficher(tete)
12 → 99 → 37 → ⌀
tete = inserer_en_tete(tete,25)
afficher(tete)
25 → 12 → 99 → 37 → ⌀
Pour supprimer en tête il suffit de remplacer la tête par l'élément suivant
def supprimer_en_tete(ancienne_tete):
assert(ancienne_tete)
nouvelle_tete = ancienne_tete.suivant
return nouvelle_tete
afficher(tete)
25 → 12 → 99 → 37 → ⌀
tete = supprimer_en_tete(tete)
afficher(tete)
12 → 99 → 37 → ⌀
Insérer ou supprimer après un maillon connu est également efficace.
Pour insérer un élément derrière le maillon M
, il faut
M
et M.suivant
def inserer_apres(M, valeur):
assert(M)
nouveau = Maillon(valeur)
nouveau.suivant = M.suivant
M.suivant = nouveau
afficher(tete)
12 → 99 → 37 → ⌀
inserer_apres(tete.suivant,42)
afficher(tete)
12 → 99 → 42 → 37 → ⌀
Pour supprimer l'élément suivant le maillon M, il suivit de faire pointer M.suivant
vers l'élément suivant celui à supprimer
def supprimer_apres(M):
assert(M != None and M.suivant != None)
M.suivant = M.suivant.suivant
afficher(tete)
12 → 99 → 42 → 37 → ⌀
supprimer_apres(tete)
afficher(tete)
12 → 42 → 37 → ⌀
Cette opération nous permet de faire des traitement complexes comme ôter les éléments se répétant en positions successives (comme std::unique
en C++)
def oter_repetitions(M):
while M != None and M.suivant != None:
if M.donnee == M.suivant.donnee:
supprimer_apres(M)
else:
M = M.suivant
afficher(zeros_un_deux)
1 → 1 → 1 → 0 → 0 → 0 → 2 → 2 → 1 → 1 → 0 → ⌀
oter_repetitions(zeros_un_deux)
afficher(zeros_un_deux)
1 → 0 → 2 → 1 → 0 → ⌀
forward_list
¶tete = inserer_en_tete(tete,25)
pour l'insertion est source d'erreur. Il est trop facile d'oublier tete =
La classe forward_list
stocke et gère le maillon de tête, plus éventuellement d'autre information comme le nombre d'éléments.
class forward_list:
def __init__(self):
self.t = None
def push_front(self,val):
self.t = inserer_en_tete(self.t,val);
def pop_front(self):
self.t = supprimer_en_tete(self.t);
def insert_after(self,m,val):
inserer_apres(m,val);
def erase_after(self,m):
supprimer_apres(m);
def empty(self): return self.t == None
L = forward_list()
for i in range(6):
L.push_front(i%3)
print(L)
2 → 1 → 0 → 2 → 1 → 0 → ⌀
L.pop_front()
print(L)
1 → 0 → 2 → 1 → 0 → ⌀
Pour rendre cette classe réellement utile, il faut pouvoir boucler sur ses éléments indépendamment de sa structure interne.
Pour cela, nous introduisons les fonctions suivantes qui cachent les notions de maillon, de tête de chaine, ...
def debut(L): return L.t
def fin(L): return None
def suivant(m): return m.suivant
def getv(m): return m.donnee
def setv(m,v): m.donnee = v
On peut ré-écrire la fonction d'affichage
def afficher(L):
m = debut(L)
while m != fin(L):
print(getv(m), end=" → ")
m = suivant(m)
print("⌀")
afficher(L)
1 → 0 → 2 → 1 → 0 → ⌀
Une fonction qui incrémente tous les éléments
def incrementer_tout(L):
m = debut(L)
while m != fin(L):
setv(m, getv(m)+1)
m = suivant(m)
print(L); incrementer_tout(L); print(L)
1 → 0 → 2 → 1 → 0 → ⌀ 2 → 1 → 3 → 2 → 1 → ⌀
Une fonction qui insère un zèro après chaque un.
def inserer_zero_apres_un(L):
m = debut(L)
while m != fin(L):
if getv(m) == 1:
L.insert_after(m,0)
m = suivant(m)
print(L); inserer_zero_apres_un(L); print(L)
2 → 1 → 3 → 2 → 1 → ⌀ 2 → 1 → 0 → 3 → 2 → 1 → 0 → ⌀
Une fonction qui supprime tous les éléments de valeur 'val'
def supprimer_valeurs(L,val):
m = debut(L)
while m != fin(L):
if suivant(m) and getv(suivant(m)) == val:
L.erase_after(m)
else: m = suivant(m)
print(L); supprimer_valeurs(L,0); print(L)
2 → 1 → 0 → 3 → 2 → 1 → 0 → ⌀ 2 → 1 → 3 → 2 → 1 → ⌀
Mais attention, cette fonction n'est pas correctement écrite. Essayons de supprimer la valeur 2
print(L); supprimer_valeurs(L,2); print(L)
2 → 1 → 3 → 2 → 1 → ⌀ 2 → 1 → 3 → 1 → ⌀
La valeur en tête n'est pas supprimée.
Il est impossible avec le faire avec erase_after
, puisqu'elle est en tête et pas après un autre maillon.
Comment faire?
supprimer_valeurs
?forward_list
! forward_list
améliorée¶Le problème étant que l'on avait pas de maillon avant la tête, la solution est simple: placer un maillon avant la tête qui
suivant
. Le plus proprement possible, il a son propre type
class MaillonVide:
def __init__(self):
self.suivant = None
class forward_list:
def __init__(self):
self.av = MaillonVide() # avant tête
def insert_after(self,m,val):
inserer_apres(m,val);
def erase_after(self,m):
supprimer_apres(m);
def push_front(self,val):
self.insert_after(self.av,val)
def pop_front(self):
self.erase_after(self.av)
def empty(self):
return self.av.suivant == None
Pour itérer, on introduit une nouvelle fonction avant_debut
et on redéfinit debut
. Les autres sont inchangées
def avant_debut(L): return L.av
def debut(L): return suivant(avant_debut(L))
def fin(L): return None
def suivant(m): return m.suivant
def getv(m): return m.donnee
def setv(m,v): m.donnee = v
La fonction supprimer_valeur
se réécrit en itérant depuis avant_debut(L)
def supprimer_valeurs(L,val):
courant = avant_debut(L)
precedent = courant
while courant != fin(L):
courant = suivant(courant)
if courant and getv(courant) == val:
L.erase_after(precedent)
else:
precedent = courant
L = forward_list()
for i in range(8):
L.push_front(i%3)
print(L)
1 → 0 → 2 → 1 → 0 → 2 → 1 → 0 → ⌀
supprimer_valeurs(L,1)
print(L)
0 → 2 → 0 → 2 → 0 → ⌀
fl_iterator
¶Il est plus pratique d'utiliser des objets que des fonctions pour itérer. La classe fl_iterator
gère un pointeur vers un des maillons
met en oeuvre getv
et setv
sépare la fonction suivant
en incr
, copie
et suivant
met en oeuvre l'opérateur d'égalité pour pouvoir arrêter nos boucles.
class fl_iterator:
def __init__(self,maillon):
self.M = maillon
def getv(self):
return self.M.donnee
def setv(self,val):
self.M.donnee = val
def incr(self):
self.M = self.M.suivant
def copie(self):
return fl_iterator(self.M)
def suivant(self, N = 1):
s = self.copie();
for i in range(N): s.incr()
return s;
def __eq__(self,other):
return isinstance(other,fl_iterator) \
and self.M == other.M
class forward_list:
def __init__(self):
self.av = MaillonVide()
def before_begin(self):
return fl_iterator(self.av)
def begin(self):
return fl_iterator(self.av.suivant)
def end(self):
return fl_iterator(None)
def insert_after(self,it,v):
inserer_apres(it.M,v);
def erase_after(self,it):
supprimer_apres(it.M);
def push_front(self,v):
self.insert_after(self.before_begin(),v)
def pop_front(self):
self.erase_after(self.before_begin())
La fonction supprimer_valeur
précédente se réécrit alors
def supprimer_valeurs(L,val):
prec = L.before_begin()
it = prec.suivant()
while it != L.end():
if it.getv() == val:
L.erase_after(prec)
else:
prec = it
it = prec.suivant()
L = forward_list()
for i in range(8):
L.push_front(i%3)
print(L);
1 → 0 → 2 → 1 → 0 → 2 → 1 → 0 → ⌀
supprimer_valeurs(L,1);
print(L)
0 → 2 → 0 → 2 → 0 → ⌀
La copie complète d'un liste peut paraitre simple, mais une approche naive ne fonctionne pas
def copie_liste_naive(L):
C = forward_list()
it = L.begin();
while it != L.end():
C.push_front(it.getv())
it.incr()
return C
L1 = forward_list()
for i in range(8):
L1.push_front(i%3)
print(L1);
1 → 0 → 2 → 1 → 0 → 2 → 1 → 0 → ⌀
L2 = copie_liste_naive(L1);
print(L2)
0 → 1 → 2 → 0 → 1 → 2 → 0 → 1 → ⌀
Pour ne pas inverser, il faut insérer en queue - pas en tête - donc se souvenir du dernier élément de la liste en construction.
def copie_liste(L):
C = forward_list()
it = L.begin();
last_of_C = C.before_begin()
while it != L.end():
C.insert_after(last_of_C,it.getv())
last_of_C.incr()
it.incr()
return C
print(L1);
1 → 0 → 2 → 1 → 0 → 2 → 1 → 0 → ⌀
L2 = copie_liste(L1);
print(L2)
1 → 0 → 2 → 1 → 0 → 2 → 1 → 0 → ⌀
Une liste simplement chainée utilise comme maillons des structures stockant individuellement
Les opérations efficaces en $\Theta(1)$ sont
Il n'y a ni pas d'accès indexé ou d'opération en queue.
La mise en oeuvre de l'itération et l'utilisation d'un noeud vide en tête simplifient l'utilisation de la structure.
© Olivier Cuisenaire, 2018 |